#include <time.h>
#include <string.h>
#include <stdio.h>
#include <sstream>
#include <iomanip>
#include <vector>
#include "BBS2chProxyAuth.h"
#include "BBS2chProxyHttpHeaders.h"
#include "BBS2chProxyConnection.h"
#include "hmac.h"
#include "utils.h"

#define SID_UPDATE_INTERVAL (60*60)

extern char *user_agent;
extern char *appKey;
extern char *hmacKey;
extern BBS2chProxyHttpHeaders api_auth_headers;
extern char *api_server;

extern void log_printf(int level, const char *format ...);

static size_t header_callback_acorn(char *buffer, size_t size, size_t nitems, void *userdata)
{
	std::string *acornCookie = static_cast<std::string *>(userdata);
	if (acornCookie && acornCookie->empty()) {
		PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(buffer, size*nitems);
		if (parsedHeader && parsedHeader->getLowercasedName() == "set-cookie") {
			const std::string acornStr = "acorn=";
			const std::string &value = parsedHeader->getValue();
			if (std::equal(acornStr.begin(), acornStr.end(), value.begin())) {
				size_t pos = value.find(";");
				if (pos == std::string::npos) *acornCookie = value.substr(6);
				else *acornCookie = value.substr(6, pos-6);
			}
		}
	}
	return size*nitems;
}

static size_t header_callback_be(char *buffer, size_t size, size_t nitems, void *userdata)
{
	std::pair<std::string, std::string> *beCookies = static_cast<std::pair<std::string, std::string> *>(userdata);
	if (beCookies) {
		PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(buffer, size*nitems);
		if (parsedHeader && parsedHeader->getLowercasedName() == "set-cookie") {
			if (beCookies->first.empty()) {
				const std::string dmdmStr = "DMDM=";
				const std::string &value = parsedHeader->getValue();
				if (std::equal(dmdmStr.begin(), dmdmStr.end(), value.begin())) {
					size_t pos = value.find(";");
					if (pos == std::string::npos) beCookies->first = value.substr(5);
					else beCookies->first = value.substr(5, pos-5);
				}
			}
			if (beCookies->second.empty()) {
				const std::string mdmdStr = "MDMD=";
				const std::string &value = parsedHeader->getValue();
				if (std::equal(mdmdStr.begin(), mdmdStr.end(), value.begin())) {
					size_t pos = value.find(";");
					if (pos == std::string::npos) beCookies->second = value.substr(5);
					else beCookies->second = value.substr(5, pos-5);
				}
			}
		}
	}
	return size*nitems;
}

static size_t header_callback_uplift(char *buffer, size_t size, size_t nitems, void *userdata)
{
	std::string *sidCookie = static_cast<std::string *>(userdata);
	if (sidCookie && sidCookie->empty()) {
		PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(buffer, size*nitems);
		if (parsedHeader && parsedHeader->getLowercasedName() == "set-cookie") {
			const std::string sidStr = "sid=";
			const std::string &value = parsedHeader->getValue();
			if (std::equal(sidStr.begin(), sidStr.end(), value.begin())) {
				size_t pos = value.find(";");
				if (pos == std::string::npos) *sidCookie = value.substr(4);
				else *sidCookie = value.substr(4, pos-4);
			}
		}
	}
	return size*nitems;
}

bool BBS2chProxyAuth::updateSID(CURL *curl)
{
	time_t ct = time(0);
	bool ret = false;
	bool shouldReleaseCurlHandle = false;
	pthread_mutex_lock(&mutex);
	if(ct < expire) {
		ret = true;
		goto last;
	}
	if (!curl) {
		curl = curl_easy_init();
		shouldReleaseCurlHandle = true;
	}
	if(curl) {
		CURLcode res;
		struct curl_slist *headers = NULL;
		std::vector<char> dat;
		char gateway[1024];
		snprintf(gateway,1024,"https://%s/v1/auth/",api_server);
		std::set<std::string> excludes;
		configureCurlHandle(curl);
		curl_easy_setopt(curl, CURLOPT_URL, gateway);
		if (api_auth_headers.has("Accept-Encoding")) {
			excludes.insert("accept-encoding");
			curl_easy_setopt(curl, CURLOPT_ENCODING, api_auth_headers.get("Accept-Encoding").c_str());
		}
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_download);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &dat);
		if (!api_auth_headers.has("User-Agent")) {
			curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0");
		}
		if (!api_auth_headers.empty()) headers = api_auth_headers.appendToCurlSlist(headers, excludes);
		if(headers) curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
		curl_easy_setopt(curl, CURLOPT_POST, 1L);
		
		std::stringstream postData;
		std::stringstream msg;
		postData << "ID=&PW=&KY=" << appKey << "&CT=" << ct << "&HB=";
		msg << appKey << ct;
		
		unsigned char digest[32];
		proxy2ch_HMAC_SHA256(hmacKey, strlen(hmacKey), msg.str().c_str(), msg.str().length(), digest);
		for(int i=0;i<32;i++) {
			postData << std::hex << std::setw(2) << std::setfill('0') << (unsigned int)digest[i];
		}
#if LIBCURL_VERSION_NUM >= 0x071101
		curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, postData.str().c_str());
#else
		std::string postDataStr = postData.str();
		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postDataStr.c_str());
#endif
		
		res = curl_easy_perform(curl);
		if(res == CURLE_OK) {
			long statusCode;
			curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE, &statusCode);
			//fprintf(stderr,"%ld\n",statusCode);
			if(statusCode == 200) {
				dat.push_back('\0');
				char *ptr = strchr(&dat.front(),':');
				if(ptr && !strncmp(&dat.front(),"SESSION-ID=Monazilla",20)) {
					sid = std::string(ptr+1);
					expire = ct + SID_UPDATE_INTERVAL;
					ret = true;
					log_printf(1,"Updated SID: %s\n",sid.c_str());
				}
				else {
					log_printf(0,"Cannot update SID: %s\n",&dat.front());
				}
			}
			else {
				log_printf(0,"Cannot update SID: API gateway returned %ld\n",statusCode);
			}
		}
		else log_printf(0,"curl error while updating SID: %s\n",curl_easy_strerror(res));
		curl_slist_free_all(headers);
		if (shouldReleaseCurlHandle) curl_easy_cleanup(curl);
		else curl_easy_reset(curl);
	}
last:
	pthread_mutex_unlock(&mutex);
	return ret;
}

std::string BBS2chProxyAuth::requestBodyForURL(const char *url, CURL *curl)
{
	if (!updateSID(curl) || sid.empty()) return "";
	std::stringstream postData;
	std::stringstream msg;
	postData << "sid=" << sid << "&appkey=" << appKey << "&hobo=";
	msg << strstr(url,"/v1/") << sid << appKey;
	
	unsigned char digest[32];
	proxy2ch_HMAC_SHA256(hmacKey, strlen(hmacKey), msg.str().c_str(), msg.str().length(), digest);
	for(int i=0;i<32;i++) {
		postData << std::hex << std::setw(2) << std::setfill('0') << (unsigned int)digest[i];
	}
	
	return postData.str();
}

const std::string& BBS2chProxyAuth::getSID()
{
	updateSID(NULL);
	return sid;
}

void IBBS2chProxyCookieAuth::prepareForLogin(CURL *curl, const std::string &authURL, BBS2chProxyFormData &requestBody)
{
	configureCurlHandle(curl);
	curl_easy_setopt(curl, CURLOPT_URL, authURL.c_str());
	curl_easy_setopt(curl, CURLOPT_ENCODING, "");
	curl_easy_setopt(curl, CURLOPT_POST, 1L);
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.toString().c_str());
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_download);
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL);
	if (user_agent) curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
	else curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0");
}

bool BBS2chProxyAcornAuth::login()
{
	bool ret = false;
	CURL *curl = curl_easy_init();
	std::string cookie;
	BBS2chProxyFormData requestBody;
	requestBody.append("email", _mail);
	requestBody.append("pass", _passwd);
	prepareForLogin(curl, "https://donguri.5ch.net/login", requestBody);
	curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback_acorn);
	curl_easy_setopt(curl, CURLOPT_HEADERDATA, &cookie);
	CURLcode res = curl_easy_perform(curl);
	if (res == CURLE_OK) {
		long statusCode;
		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
		if (statusCode == 302) {
			log_printf(0, "Logged in to donguri system successfully.\n");
			log_printf(1, "New cookie is: %s\n",cookie.c_str());
			pthread_mutex_lock(&_mutex);
			_cookie = cookie;
			_lastUpdated = time(NULL);
			pthread_mutex_unlock(&_mutex);
			ret = true;
		} else {
			if (statusCode == 200) log_printf(0, "Cannot login to donguri system (maybe incorrect user/password or too many requests).\n");
			else log_printf(0, "Cannot login to Donguri system: server returned %ld\n", statusCode);
		}
	}
	else {
		log_printf(0, "curl error: %s\n", curl_easy_strerror(res));
	}
	curl_easy_cleanup(curl);
	pthread_mutex_lock(&_mutex);
	_isUpdating = false;
	pthread_mutex_unlock(&_mutex);
	return ret;
}

int IBBS2chProxyCookieAuth::loginWith(const std::string &mail, const std::string &passwd)
{
	pthread_mutex_lock(&_mutex);
	if (!_isUpdating) {
		_isUpdating = true;
		_mail = mail;
		_passwd = passwd;
		pthread_mutex_unlock(&_mutex);
		return login() ? 0 : -1;
	}
	pthread_mutex_unlock(&_mutex);
	return 1;
}

void BBS2chProxyAcornAuth::logout()
{
	pthread_mutex_lock(&_mutex);
	_mail.clear();
	_passwd.clear();
	_cookie.clear();
	_lastUpdated = time(NULL);
	pthread_mutex_unlock(&_mutex);
}

static void *doLogin(void *auth)
{
	((IBBS2chProxyCookieAuth *)auth)->login();
	return NULL;
}

void IBBS2chProxyCookieAuth::update(bool force)
{
	if (!isLoggedIn()) return;
	pthread_mutex_lock(&_mutex);
	if (!_isUpdating && (force || time(NULL) >= _lastUpdated + _updateInterval)) {
		_isUpdating = true;
		pthread_t thread;
		pthread_create(&thread, NULL, &doLogin, this);
		pthread_detach(thread);
	}
	pthread_mutex_unlock(&_mutex);
}

bool BBS2chProxyAcornAuth::syncCookie(BBS2chProxyKeyManager::CookieJar &jar)
{
	if (jar.timeAcornSynced >= _lastUpdated) {
		update(false);
		return false;
	}
	BBS2chProxyKeyManager::Cookie cookie;
	cookie.name = "acorn";
	cookie.value = _cookie;
	cookie.domain = ".5ch.net";
	if (_cookie.empty()) cookie.expires = "1";
	jar.lock();
	jar.set(cookie);
	jar.timeAcornSynced = _lastUpdated;
	jar.unlock();
	return true;
}

std::string IBBS2chProxyCookieAuth::responseHTML()
{
	std::string html;
	html += "<div class=\"container\"><h3>";
	html += _headerTitle;
	html += "にログイン</h3>";
	if (!isLoggedIn()) {
		html += "<form id=\"";
		html += _uniqueId;
		html += "-login\"action=\"accounts\" method=\"POST\">";
		html += "<input type=\"text\" name=\"mail\" placeholder=\"メールアドレス\">";
		html += "<input type=\"password\" name=\"pass\" placeholder=\"パスワード\">";
		html += "<input type=\"hidden\" name=\"action\" value=\"login\">";
		html += "<input type=\"hidden\" name=\"service\" value=\"";
		html += _uniqueId;
		html += "\"><button type=\"submit\">ログイン</button></form></div>";
	} else {
		std::ostringstream ss;
		time_t ago = time(NULL) - _lastUpdated;
		html += "<p>";
		if (ago < 3600) ss << ago/60 << " 分前にログイン済みです。</p>";
		else ss << ago/3600 << " 時間前にログイン済みです。</p>";
		html += ss.str();
		html += "<form action=\"accounts\" method=\"POST\">";
		html += "<input type=\"hidden\" name=\"action\" value=\"logout\">";
		html += "<input type=\"hidden\" name=\"service\" value=\"";
		html += _uniqueId;
		html += "\"><button type=\"submit\">ログアウト</button></form>";
	}
	html += "</div>";
	return html;
}

bool BBS2chProxyAcornAuth::isLoggedIn()
{
	return !_cookie.empty();
}

bool BBS2chProxyBeAuth::login()
{
	bool ret = false;
	CURL *curl = curl_easy_init();
	std::pair<std::string, std::string> cookies;
	BBS2chProxyFormData requestBody;
	requestBody.append("mail", _mail);
	requestBody.append("pass", _passwd);
	prepareForLogin(curl, "https://be.5ch.net/log", requestBody);
	curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback_be);
	curl_easy_setopt(curl, CURLOPT_HEADERDATA, &cookies);
	CURLcode res = curl_easy_perform(curl);
	if (res == CURLE_OK) {
		long statusCode;
		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
		if (statusCode == 302) {
			if (!cookies.first.empty() && !cookies.second.empty()) {
				log_printf(0, "Logged in to be successfully.\n");
				log_printf(1, "New cookie is: DMDM=%s, MDMD=%s\n", cookies.first.c_str(), cookies.second.c_str());
				pthread_mutex_lock(&_mutex);
				_dmdm = cookies.first;
				_mdmd = cookies.second;
				_lastUpdated = time(NULL);
				pthread_mutex_unlock(&_mutex);
				ret = true;
			}
			else log_printf(0, "Cannot login to be (maybe incorrect user/password).\n");
		} else {
			log_printf(0, "Cannot login to be: server returned %ld\n", statusCode);
		}
	}
	else {
		log_printf(0, "curl error: %s\n", curl_easy_strerror(res));
	}
	curl_easy_cleanup(curl);
	pthread_mutex_lock(&_mutex);
	_isUpdating = false;
	pthread_mutex_unlock(&_mutex);
	return ret;
}

void BBS2chProxyBeAuth::logout()
{
	pthread_mutex_lock(&_mutex);
	_mail.clear();
	_passwd.clear();
	_dmdm.clear();
	_mdmd.clear();
	_lastUpdated = time(NULL);
	pthread_mutex_unlock(&_mutex);
}

bool BBS2chProxyBeAuth::syncCookie(BBS2chProxyKeyManager::CookieJar &jar)
{
	if (jar.timeBeSynced >= _lastUpdated) {
		update(false);
		return false;
	}
	BBS2chProxyKeyManager::Cookie dmdm, mdmd;
	dmdm.name = "DMDM";
	dmdm.value = _dmdm;
	dmdm.domain = ".5ch.net";
	if (_dmdm.empty()) dmdm.expires = "1";
	mdmd.name = "MDMD";
	mdmd.value = _mdmd;
	mdmd.domain = ".5ch.net";
	if (_mdmd.empty()) mdmd.expires = "1";
	jar.lock();
	jar.set(dmdm);
	jar.set(mdmd);
	jar.timeBeSynced = _lastUpdated;
	jar.unlock();
	return true;
}

bool BBS2chProxyBeAuth::isLoggedIn()
{
	return !_dmdm.empty() && !_mdmd.empty();
}

bool BBS2chProxyUpliftAuth::login()
{
	bool ret = false;
	CURL *curl = curl_easy_init();
	std::string cookie;
	BBS2chProxyFormData requestBody;
	requestBody.append("usr", _mail);
	requestBody.append("pwd", _passwd);
	prepareForLogin(curl, "http://uplift.5ch.net/log", requestBody);
	curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback_uplift);
	curl_easy_setopt(curl, CURLOPT_HEADERDATA, &cookie);
	CURLcode res = curl_easy_perform(curl);
	if (res == CURLE_OK) {
		long statusCode;
		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
		if (!cookie.empty()) {
			log_printf(0, "Logged in to uplift successfully.\n");
			log_printf(1, "New cookie is: %s\n", cookie.c_str());
			pthread_mutex_lock(&_mutex);
			_cookie = cookie;
			_lastUpdated = time(NULL);
			pthread_mutex_unlock(&_mutex);
			ret = true;
		} else {
			if (statusCode == 200) log_printf(0, "Cannot login to uplift (maybe incorrect user/password).\n");
			else log_printf(0, "Cannot login to uplift: server returned %ld\n", statusCode);
		}
	}
	else {
		log_printf(0, "curl error: %s\n", curl_easy_strerror(res));
	}
	curl_easy_cleanup(curl);
	pthread_mutex_lock(&_mutex);
	_isUpdating = false;
	pthread_mutex_unlock(&_mutex);
	return ret;
}

void BBS2chProxyUpliftAuth::logout()
{
	pthread_mutex_lock(&_mutex);
	_mail.clear();
	_passwd.clear();
	_cookie.clear();
	_lastUpdated = time(NULL);
	pthread_mutex_unlock(&_mutex);
}

bool BBS2chProxyUpliftAuth::syncCookie(BBS2chProxyKeyManager::CookieJar &jar)
{
	if (jar.timeBeSynced >= _lastUpdated) {
		update(false);
		return false;
	}
	BBS2chProxyKeyManager::Cookie for5ch, forPink;
	for5ch.name = "sid";
	for5ch.value = _cookie;
	for5ch.domain = ".5ch.net";
	forPink.name = "sid";
	forPink.value = _cookie;
	forPink.domain = ".bbspink.com";
	if (_cookie.empty()) {
		for5ch.expires = "1";
		forPink.expires = "1";
	}
	jar.lock();
	jar.set(for5ch);
	jar.set(forPink);
	jar.timeUpliftSynced = _lastUpdated;
	jar.unlock();
	return true;
}

bool BBS2chProxyUpliftAuth::isLoggedIn()
{
	return !_cookie.empty();
}

int BBS2chProxyCookieAuthManager::loginWith(const std::string &service, const std::string &mail, const std::string &passwd)
{
	if (service == "donguri") return _acornAuth.loginWith(mail, passwd);
	else if (service == "be") return _beAuth.loginWith(mail, passwd);
	else if (service == "uplift") return _upliftAuth.loginWith(mail, passwd);
	return 2;
}

void BBS2chProxyCookieAuthManager::logout(const std::string &service)
{
	if (service == "donguri") _acornAuth.logout();
	else if (service == "be") _beAuth.logout();
	else if (service == "uplift") _upliftAuth.logout();
}

void BBS2chProxyCookieAuthManager::syncCookies(BBS2chProxyKeyManager::CookieJar &jar)
{
	bool shouldFlush = false;
	if (_acornAuth.syncCookie(jar)) shouldFlush = true;
	if (_beAuth.syncCookie(jar)) shouldFlush = true;
	if (_upliftAuth.syncCookie(jar)) shouldFlush = true;
	if (shouldFlush) BBS2chProxyConnection::keyManager.flushCookies();
}

std::string BBS2chProxyCookieAuthManager::responseHTML()
{
	std::string html;
	html += "<!DOCTYPE html><html lang=\"ja\"><head>";
	html += "<title>proxy2ch: アカウント管理</title>";
	html += "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">";
	html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><style>";
	html += ".container { width: 300px; margin: 0 auto; padding: 20px; border: 1px solid #ccc; border-radius: 5px; margin-bottom: 10px; }";
	html += "h3 { text-align: center; margin-bottom: 20px; margin-top: 0px; }";
	html += "input[type=\"text\"], input[type=\"password\"] { width: 93%; padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }";
	html += "button[type=\"submit\"] { width: 100%; padding: 10px; background-color: #4CAF50; color: #ffffff; border: none; border-radius: 4px; cursor: pointer; }";
	html += "</style></head><body>";
	html += _acornAuth.responseHTML();
	html += _beAuth.responseHTML();
	html += _upliftAuth.responseHTML();
	html += "</body></html>\n";
	return html;
}
